package racer.server;

import java.net.ServerSocket;
import java.net.InetSocketAddress;

import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.regex.Matcher;

import java.io.IOException;
import java.nio.channels.ClosedChannelException;

import racer.client.data.RegEx;
/** 
 * 
 * RaceServer class
 * uses new I/O classes with nonblocking sockets 
 *
 */
public class RaceServer {

	private static int PORT = 2222;
	private final int MAXRACES = 10;
	private static int RACEID = 0;
	private static int PLAYERID = 0;
	private static final String DELIM = ":=:";
	
	private String bufferMessage="";
	
	private HashMap<SocketChannel, Player> playersBySocket;
	private HashMap<Integer, Player> playersByID;
	private HashMap<Integer, Race> races;
	private HashMap<Integer, Integer> playerRaces;
	
	private Selector selector;
	private ServerSocketChannel servChann;
	private ServerSocket servSock;
	
	public RaceServer() {
		playersBySocket = new HashMap<SocketChannel,Player>();
		playersByID = new HashMap<Integer,Player>();
		playerRaces = new HashMap<Integer, Integer>();
		races = new HashMap<Integer, Race>();
		try {
			servChann = ServerSocketChannel.open();
			servChann.configureBlocking(false);
			
			servSock = servChann.socket();
			servSock.bind(new InetSocketAddress(PORT));
			
			selector = Selector.open();
			servChann.register(selector, SelectionKey.OP_ACCEPT);
		} catch (IOException e) {
			e.printStackTrace();
		}
		try {
			doServerStuff();
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}
	
	/**
	 * It is critical that the actions related to the event of a 
	 * SelectionKey, e.g. newPlayer() and recvMessage(), will not
	 * block somewhere
	 * 
	 * @throws IOException
	 */
	private void doServerStuff() throws IOException {
		while(true) {
			System.out.println("==================\nWaiting for select()");
			int numselkeys = selector.select();
			Iterator it = selector.selectedKeys().iterator();
			SelectionKey key;
			System.out.println("select() returned: " + numselkeys + " // " + selector.selectedKeys().size() + " selected keys according to selectedKeys()");
			while (it.hasNext()) {
				key = (SelectionKey)it.next();
				it.remove();
				if (key.isAcceptable())
					newPlayer(selector, key);
				else if (key.isReadable())
					recvMessage(key);
				else if (key.isWritable())
					System.out.println("key is writable");
				else if (key.isConnectable())
					System.out.println("key is connectable");
				else if(!key.isValid())
					System.out.println("invalid key");
				else
					System.out.println("Unknown key operation on : " + key);
			}
		}
	}
	
	/**
	 * Accept client, register client with selector and put client in hashmap
	 * @param selector
	 * @param key
	 */
	private void newPlayer(Selector selector, SelectionKey key) {
		System.out.println("new player");
		try {
			ServerSocketChannel servChann = (ServerSocketChannel)key.channel();
			SocketChannel sockChann = servChann.accept();
			
			sockChann.configureBlocking(false);
			sockChann.register(selector, SelectionKey.OP_READ);
			
			int playerid = getPlayerId();
			Player p = new Player(playerid, sockChann, this);
			playersBySocket.put(sockChann, p);
			playersByID.put(playerid, p);
			p.sendMessage(new racer.client.data.LoginMessage().getFormedMessage(playerid));
			
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
		
	}
	
	// gets message from player, removes newline and does something with it
	private void recvMessage(SelectionKey key) {
		Player p = (Player)playersBySocket.get((SocketChannel)key.channel());
		if(p == null)
			return;
		String msg = p.recvMessage();
		
		if (msg != null) {
			// message from buffer may consist of multiple messages, split them and process each of them
			bufferMessage+=msg;
			int firstPos = bufferMessage.indexOf(DELIM);
			int nextPos = bufferMessage.indexOf(DELIM, firstPos+1);
			while(nextPos != -1) {
				String retmsg = bufferMessage.substring(firstPos, nextPos + DELIM.length());
				processMessage(p, retmsg);
				bufferMessage = bufferMessage.substring(nextPos+DELIM.length());
				firstPos = bufferMessage.indexOf(DELIM);
				nextPos = bufferMessage.indexOf(DELIM, firstPos+1);
			}
			
			//System.out.println("received message:");
			//System.out.println(msg);
			//processMessage(p, msg);
			//sendBroadcastMessage(msg, p.getSockChann());
		}
	}
	
	private void processMessage(Player p, String msg) {
		//sendBroadcastMessage(msg, p.getSockChann());
		//if(true)
		//	return;

		Matcher vergelijkmsgid = RegEx.getMatcher(RegEx.id_expression,msg);		
		// find out message type
		int msgtype = -1;
		if (RegEx.Matches(vergelijkmsgid,0)) {
			try {
				msgtype = Integer.parseInt(vergelijkmsgid.group(1));
			} catch (NumberFormatException e) {
				e.printStackTrace();
			}
		} else
			return;
		
		if(msgtype == -1)
			return;
		
		// create Matcher object to retrieve all information
		Matcher vergelijk = RegEx.getMatcher(RegEx.getExpression(msgtype), msg);
		if(vergelijk == null)
			return;
		if(!vergelijk.find(0))
				return;
		
		switch(msgtype) {
		// position
		case 1:
			try {
				int playerid = Integer.parseInt(vergelijk.group(2));
				int raceid = playerRaces.get(playerid);
				Race race = races.get(raceid);
				ArrayList<Integer> players = race.getPlayers();
				for(int i=0; i<players.size(); i++) {
					Player player = playersByID.get(players.get(i));
					if(player != null && player.getId() != playerid)
						player.sendMessage(msg);
				}
			} catch (NullPointerException e) {
				e.printStackTrace();
			} catch (NumberFormatException nfe) {
				nfe.printStackTrace();
			}
			
			break;
		// chat
		case 2: break;
		// settings
		case 3: break;
		// create race
		case 4: 
			try {
				//int playerid = Integer.parseInt(vergelijk.group(2));
				String mapName = vergelijk.group(3);
				int maxPlayers = Integer.parseInt(vergelijk.group(4));
				createRace(mapName, null, maxPlayers);
			} catch (NullPointerException e) {
				e.printStackTrace();
			} catch (NumberFormatException nfe) {
				nfe.printStackTrace();
			}
			break;
		// join race
		case 5: 
			try {
				int playerid = Integer.parseInt(vergelijk.group(2));
				int raceid = Integer.parseInt(vergelijk.group(3));
				joinRace(raceid, playerid);
			} catch (NumberFormatException nfe) {
				nfe.printStackTrace();
			}
			break;
		// list races request
		case 6: 
			String returnmsg = listRaces(p.getId());
			p.sendMessage(returnmsg);
			break;
		case 8:
			try {
				int playerid = Integer.parseInt(vergelijk.group(2));
				int raceid = playerRaces.get(playerid);
				Race race = races.get(raceid);
				ArrayList<Integer> players = race.getPlayers();
				for(int i=0; i<players.size(); i++) {
					Player player = playersByID.get(players.get(i));
					if(player != null && player.getId() != playerid)
						player.sendMessage(msg);
				}
			} catch (NullPointerException e) {
				e.printStackTrace();
			} catch (NumberFormatException nfe) {
				nfe.printStackTrace();
			}			
			break;
		case 10:
			String mapName = vergelijk.group(2);
			String map = vergelijk.group(3);
			receivedMap(mapName, map);
			break;
		default:
			System.out.println("Invalid message");
			break;	
		}
	}
	
	private void createRace(String mapName, String gameType, int maxPlayers) {
		int raceid = getRaceKey();
		races.put(raceid, new Race(raceid, mapName, gameType, maxPlayers));
	}
	private int getRaceKey() {
		return ++RACEID;
	}
	private int getPlayerId() {
		return ++PLAYERID;
	}
	
	private String listRaces(int fromId) {
		String raceslist = ":=:7;fromID=" + fromId + ";";
		Iterator i = races.keySet().iterator();
		while(i.hasNext()) {
			Race race = races.get(i.next());
			raceslist += "raceID=" + race.getRaceid() + ";";
			raceslist += "mapName=" + race.getMapName() + ";";
			raceslist += "numPlayers=" + race.getNumberOfPlayers() + ";";
			raceslist += "maxPlayers=" + race.getMaxPlayers() + ";";
		}
		raceslist += "raceid=-1:=:";
		System.out.println("Races list (outgoing message): " + raceslist);
		return raceslist;
	}
	
	private void joinRace(int raceid, int playerid) {
		Race race = races.get(raceid);
		race.addPlayer(playerid);
		playerRaces.put(new Integer(playerid), new Integer(raceid));		
		int countdown = 5;
		Player p = playersByID.get(playerid);
		p.sendMessage(":=:10;mapName=" + race.getMapName() 
				+ ";mapString=" 
				+ racer.client.data.FileHandlers.openFileHandler(new java.io.File("maps/" + race.getMapName())) 
				+ ":=:");
		while(countdown > 0) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {}
			countdown--;
		}
		if(race.getNumberOfPlayers() == race.getMaxPlayers()) {
			ArrayList<Integer> players = race.getPlayers();
			for(int i=0; i<players.size(); i++) {
				Player player = playersByID.get(players.get(i));
				if(player != null)
					player.sendMessage(":=:9;raceID=" + raceid + ":=:");
			}
		}
	}
	
	private void receivedMap(String mapName, String map) {
		racer.client.data.FileHandlers.saveFileHandler(mapName,map);
	}
	
	private void sendMessage(Race race) {
		
	}
	
	// this might be done more efficiently by using the same buffer and just
	// rewinding it
	private void sendBroadcastMessage(String msg, SocketChannel from) {
		System.out.println("Broadcasting message: " + msg);
		Iterator i = playersBySocket.values().iterator();
		while(i.hasNext()) {
			Player p = (Player)i.next();
			if (p.getSockChann() != from)
				p.sendMessage(msg);
		}
	}
	
	public void removeChannel(SocketChannel sockChann, int id) {
		System.out.println("Removing channel");
		try {
			sockChann.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		playersBySocket.remove(sockChann);
		playersByID.remove(id);
		Race race = races.get(playerRaces.get(id));
		if(race != null)
			race.removePlayer(id);
	}
	
	/*
	public HashMap<SocketChannel,Player> getPlayers() {
		return playersBySocket;
	}*/
	
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		new RaceServer();
	}

}
